package utsbia;

import java.awt.*;
import java.awt.geom.*;
import java.io.IOException;
import java.util.*;
 
import robocode.*;
import robocode.util.*;
 
public class LeaderWaveRobot extends TeamRobot {
	final static float MAX_SPEED = 8;
	final static double BULLET_DAMAGE = 10;
	final static double SHOOTING_ANGLE = Math.PI / 16f;
	double ENEMY_CORRECTION_VALUE = 0d;
	final static int DISTANCE_FROM_BOUNDARY = 50;
	final static int REPEL_DISTANCE_FROM_BOUNDARY = 100;
	
	/*
	 * change these to see different graphics.
	 */
	final static boolean PAINT_MOVEMENT = true;
	final static boolean PAINT_GUN = true;
 
	static double enemyEnergy;
 
	/*
	 * The waves for movement and aiming are stored in the following array lists.
	 */
	ArrayList<LeaderWaveRobot.MovementWave> moveWaves=new ArrayList<LeaderWaveRobot.MovementWave>();
	ArrayList<LeaderWaveRobot.GunWave> gunWaves=new ArrayList<LeaderWaveRobot.GunWave>();
 
	/*
	 * This Array will hold the most recent movement angle for every velocity segment;
	 */
	static double gunAngles[] = new double[16];
	
	/*
	 * The robotId is used to distinguish communicating robots.
	 */
	protected long robotId;
	
	/*
	 * Positions of friendly robots are stored in a map.
	 * Through the use of the robotId as a key old positions will be overwritten.
	 */
	protected Map<Long, Point2D.Double> friendlyPositions;
	
	public LeaderWaveRobot() {
		Random randomGenerator = new Random();
		// robotID is randomly assigned to give unique identifier in the team.
		robotId = randomGenerator.nextLong();
		
		friendlyPositions = new HashMap<Long, Point2D.Double>();
	}
	
	public void onStatus(StatusEvent e) {
		// Every turn the own position is sent to friends.
		try {
			broadcastMessage(new FriendlyPositionMessage(
					new Point2D.Double(getX(), getY()), 
					robotId));
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
	
	public void run(){
		enemyEnergy = 100;
 
		// Radar and gun should be moved independently from each other and robot movement.
		setAdjustGunForRobotTurn(true);
		setAdjustRadarForGunTurn(true);
		setColors(Color.white,Color.gray,Color.red);
 
		// If radar lock has been lost, start turning the radar.
		while(true) {
			if(getRadarTurnRemainingRadians() == 0) {
				setTurnRadarRightRadians(Double.POSITIVE_INFINITY);
			}
			// This method paints the waves.
			paint();
			// Perform all pending actions.
			execute();
		}
	}
	
	public void onScannedRobot(ScannedRobotEvent e){
		
		// Prevent friendly fire.
		if (isTeammate(e.getName())) {
			return;
		}
		
		// Calculate the absolute bearing to the target.
		double absoluteBearing = e.getBearingRadians() + getHeadingRadians();
		
		// Calculate the enemy's position and send it to friends.
		Point2D.Double enemyPosition = project(new Point2D.Double(getX(), getY()), e.getDistance(), absoluteBearing);
		try {
			// Send the location of the scanned enemy to team mates.
			broadcastMessage(new EnemyPositionMessage(enemyPosition));
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		// Give the other robots in the team the order to shoot at the enemy.
		try {
			broadcastMessage(new EnemyAimOrderMessage(
					enemyPosition, 
					e.getVelocity(), 
					e.getBearingRadians(),
					e.getEnergy()));
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		/*
		 * If there is an energy change on the enemy's side it has probably shot at us.
		 * The information about the bullet is stored as a wave for movement.
		 */
		double energyChange = (enemyEnergy - (enemyEnergy = e.getEnergy()));
		if((energyChange <= 3) && (energyChange >= 0.1)){
			notifyOfEnemyBullet(e, energyChange);
		}
		/*
		 * After we are done checking to see if we need to log any waves, we'll decide where to move.
		 */
		chooseDirection(project(new Point2D.Double(getX(), getY()), e.getDistance(), absoluteBearing));
 
		/*
		 * Old waves that have passed the enemy are removed.
		 */
		checkFiringWaves(project(new Point2D.Double(getX(),getY()),e.getDistance(),absoluteBearing));
 
		/*
		 * Aiming our gun and firing
		 */
		setTurnGunRightRadians(Utils.normalRelativeAngle(absoluteBearing - getGunHeadingRadians())
				+gunAngles[8 + (int)(e.getVelocity() * Math.sin(e.getHeadingRadians() - absoluteBearing))]);
		
		// Calculate the aiming certainty heuristic to determine fire power.
		double aimingCertainty = aimingCertainty(e) * 4;
		
		/*
		 * Store information about own bullet and send it to friends to avoid friendly fire.
		 */
		if(getGunHeat() == 0){
			notifyOfFriendlyBullet(e, aimingCertainty);
		}
		
		setFire(aimingCertainty);		
		
		// Turn the radar to keep lock on enemy.
		setTurnRadarRightRadians(Utils.normalRelativeAngle(absoluteBearing - getRadarHeadingRadians()) * 2);
	}
	
	/*
	 * Respond to messages from friends.
	 */
	public void onMessageReceived(MessageEvent e) {
		// Store information about friend's bullets as normal bullet waves.
		if (e.getMessage() instanceof FriendlyBulletMessage) {
			FriendlyBulletMessage bulletMessage = (FriendlyBulletMessage) e.getMessage();
			
			logMovementWave(bulletMessage);
		}
		// Store information about enemy bullet's as if they were aimed at us.
		else if (e.getMessage() instanceof EnemyBulletMessage) {
			EnemyBulletMessage bulletMessage = (EnemyBulletMessage) e.getMessage();
			
			logMovementWave(bulletMessage);
		}
		// Store friend's position.
		else if (e.getMessage() instanceof FriendlyPositionMessage) {
			FriendlyPositionMessage positionMessage = (FriendlyPositionMessage) e.getMessage();
			
			friendlyPositions.put(positionMessage.getRobotId(), positionMessage.getPosition());
		}
	}
	
	/*
	 * Log own bullet and send information to friends.
	 */
	private void notifyOfFriendlyBullet(
			ScannedRobotEvent e,
			double energy) {
		
		logFiringWave(e, energy);
		
		FriendlyBulletMessage bulletMessage = new FriendlyBulletMessage(
				new Point2D.Double(getX(),getY()),
				getTime(), 
				calculateBulletSpeed(energy), 
				e.getBearingRadians()+getHeadingRadians());		
		try {
			broadcastMessage(bulletMessage);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
	
	/*
	 * Low enemy bullet as if it was fired at us and send information to friends.
	 */
	protected void notifyOfEnemyBullet(ScannedRobotEvent e, double energyChange) {
		
		logMovementWave(e, energyChange);
		
		double absBearing = e.getBearingRadians()+getHeadingRadians();
		EnemyBulletMessage bulletMessage = new EnemyBulletMessage(project(
				new Point2D.Double(getX(), getY()), e.getDistance(), absBearing), 
				getTime(), 
				calculateBulletSpeed(energyChange));		
		try {
			broadcastMessage(bulletMessage);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
	
	/*
	 * Our bullet has hit, probably the enemy. We log the enemy's damage.
	 */
	public void onBulletHit(BulletHitEvent e){
		enemyEnergy -= BULLET_DAMAGE;
	}
	
	/*
	 * Information about incoming bullets is logged and used for robot's movement.
	 */
	public void logMovementWave(ScannedRobotEvent e,double energyChange){
		double absBearing = e.getBearingRadians() + getHeadingRadians();
		MovementWave w = new MovementWave();
		// This is the spot that the enemy was in when they fired.
		w.origin = project(new Point2D.Double(getX(), getY()), e.getDistance(), absBearing);
		// The bullet's speed can be deduced from the change in energy on the enemy's side.
		w.speed = calculateBulletSpeed(energyChange);
		// Log the time of the event to allow calculation of bullet progression.
		w.startTime = getTime();
		// The angle at, which the bullet is travelling, was it fired directly at us.
		w.angle = Utils.normalRelativeAngle(absBearing + Math.PI);
		// If the enemy used linear targeting we need the lateral velocity to calculate trajectory.
		w.latVel = (getVelocity()*Math.sin(getHeadingRadians()-w.angle))/w.speed;
		moveWaves.add(w);
	}
	
	/*
	 * This method stores a received FriendlyBulletMessage as MovementWave.
	 */
	public void logMovementWave(FriendlyBulletMessage bulletMessage) {
		MovementWave wave = new MovementWave();
		
		wave.origin = bulletMessage.getOrigin();
		wave.speed = bulletMessage.getSpeed();
		wave.startTime = bulletMessage.getStartTime();
		wave.angle = bulletMessage.getAngle();
		wave.latVel = bulletMessage.getAngle();
		
		moveWaves.add(wave);
	}
	
	/*
	 * This method stores a received EnemyBulletMessage as MovementWave.
	 */
	private void logMovementWave(EnemyBulletMessage bulletMessage) {
		MovementWave wave = new MovementWave();
		
		wave.origin = bulletMessage.getOrigin();
		wave.speed = bulletMessage.getSpeed();
		wave.startTime = bulletMessage.getStartTime();
		
		double dx = wave.origin.x - getX();
		double dy = wave.origin.y - getY();
		
		// Calculate the bearing to the enemy.
		double absBearing = Math.atan2(dx, dy);
		
		// Calculate the angle of the bullet's trajectory, had it been fired directly at us.
		wave.angle = Utils.normalRelativeAngle(absBearing + Math.PI);
		wave.latVel = (getVelocity()*Math.sin(getHeadingRadians()-wave.angle))/wave.speed;
		
		moveWaves.add(wave);
	}
	
	/*
	 * This method looks at all the directions we could go, then rates them based on different obstacles
	 * that we want to stay away from.
	 */
	public void chooseDirection(Point2D.Double enemyLocation){
		MovementWave wave;
		// Initially bestRating should be infinity. Low ratings are good.
		double bestRating = Double.POSITIVE_INFINITY;
		for(double moveAngle = 0; moveAngle < Math.PI*2; moveAngle += Math.PI / 16D){
			double rating = 0;
			
			/*
			 * movePoint is the position we would be at if we were to move one robot-length
			 * in the given direction. 
			 */
			Point2D.Double movePoint = project(new Point2D.Double(getX(), getY()), 36, moveAngle);
			
			/*
			 * Stop robot from coming close to the end of the world.
			 */
			Rectangle2D worldBoundary = new Rectangle2D.Double(DISTANCE_FROM_BOUNDARY, DISTANCE_FROM_BOUNDARY, 
					getBattleFieldWidth() - 2*DISTANCE_FROM_BOUNDARY, 
					getBattleFieldHeight() - 2*DISTANCE_FROM_BOUNDARY);
			if (!worldBoundary.contains(movePoint))
			{
				continue;
			}
			
			/*
			 * Make boundary repel robot.
			 */		
			rating += 1D / Math.pow(getBattleFieldWidth() - REPEL_DISTANCE_FROM_BOUNDARY - movePoint.x, 2);
			rating += 1D / Math.pow(movePoint.x - REPEL_DISTANCE_FROM_BOUNDARY, 2);
			rating += 1D / Math.pow(getBattleFieldHeight() - REPEL_DISTANCE_FROM_BOUNDARY - movePoint.y, 2);
			rating += 1D / Math.pow(movePoint.y - REPEL_DISTANCE_FROM_BOUNDARY, 2);
			
			/*
			 * This loop will iterate through each wave and add a risk for the simulated bullets on each one
			 * to the total risk for this angle.
			 */
			for(int i = 0; i < moveWaves.size(); i++){
				wave = moveWaves.get(i);
				
				/*
				 * This part will remove waves that have passed our robot, so we no longer keep taking 
				 * into account old ones.
				 */
				if(new Point2D.Double(getX(), getY()).distance(wave.origin) < 
						(getTime() - wave.startTime) * wave.speed+wave.speed)
				{
					moveWaves.remove(wave);
				}
				else{
					/*
					 * This adds multiple risks to the position to avoid a broader range around the 
					 * suspected bullet position.
					 */
					rating += 1D / Math.pow(movePoint.distance(project(wave.origin, movePoint.distance(wave.origin), wave.angle + ENEMY_CORRECTION_VALUE)), 1.8);
					rating += 1D / Math.pow(movePoint.distance(project(wave.origin, movePoint.distance(wave.origin), wave.angle)), 1.8);
					rating += 1D / Math.pow(movePoint.distance(project(wave.origin, movePoint.distance(wave.origin), wave.angle - ENEMY_CORRECTION_VALUE)), 1.8);
				}
			}
			
			// Avoid collisions with friendly robots.
			for (Point2D.Double robotPosition: friendlyPositions.values()) {
				rating += 1D / Math.pow(movePoint.distance(robotPosition), 1.8);
			}
			
			// This adds a risk associated with being to close to the other robot if there are no waves.
			if (moveWaves.size() == 0) {
				rating += 1D / Math.pow(movePoint.distance(enemyLocation), 1.8);
			}
			
			/*
			 * This part tells us to go in the direction if it is better than the previous best option and
			 * is reachable.
			 */
			if(rating < bestRating)
			{ 
				bestRating = rating;
				/*
				 * These next three lines are a very codesize-efficient way to 
				 * choose the best direction for moving to a point.
				 */
				int pointDir;
				setAhead(1000 * (pointDir = (Math.abs(moveAngle - getHeadingRadians()) < Math.PI / 2 ? 1: -1)));
				setTurnRightRadians(Utils.normalRelativeAngle(moveAngle + (pointDir == -1? Math.PI: 0) - getHeadingRadians()));
			}
		}
	}
 
	/*
	 * Calculate the ENEMY_CORRECTION_VALUE to predict enemy's aiming behaviour.
	 */
	public void onHitByBullet(HitByBulletEvent event)
	{
		event.getBearingRadians();
		
		// Find the nearest wave.
		double minimumDifference = Double.MAX_VALUE;
		MovementWave nearestWave = null;
		for (MovementWave wave: moveWaves)
		{
			double differenceWaveRobot = wave.origin.distance(getX(), getY()) - (getTime() - wave.startTime) * wave.speed;
			if (differenceWaveRobot < minimumDifference)
			{
				nearestWave = wave;
				minimumDifference = differenceWaveRobot;
			}
		}
		
		if (nearestWave == null)
		{
			return; 
		}
		
		project(nearestWave.origin, (getTime() - nearestWave.startTime) * nearestWave.speed, nearestWave.angle);
		
		double anglePredicted = nearestWave.angle;
		double dx = getX() - nearestWave.origin.x;
		double dy = getY() - nearestWave.origin.y;
		double actualAngle = Math.atan2(dx, dy);
		
		ENEMY_CORRECTION_VALUE = Utils.normalRelativeAngle(actualAngle - anglePredicted);
	}
	
	/*
	 * This method will log a firing wave.
	 */
	public void logFiringWave(ScannedRobotEvent e, double energy){
		GunWave w = new GunWave();
		w.absBearing = e.getBearingRadians() + getHeadingRadians();
		w.speed = calculateBulletSpeed(energy);
		w.origin = new Point2D.Double(getX(), getY());
		w.velSeg = (int)(e.getVelocity() * Math.sin(e.getHeadingRadians() - w.absBearing));
		w.startTime = getTime();
		gunWaves.add(w);
	}

	/*
	 * Calculates a bullets speed based on the energy.
	 */
	protected double calculateBulletSpeed(double energy)
	{
		return 20 - energy * 3;	
	}
	
	/*
	 * This method checks firing waves to see if they have passed the enemy yet.
	 */
	public void checkFiringWaves(Point2D.Double ePos){
		GunWave w;
		for(int i = 0; i < gunWaves.size(); i++){
			w = gunWaves.get(i);
			if((getTime() - w.startTime) * w.speed >= w.origin.distance(ePos)){
				gunAngles[w.velSeg + 8] = Utils.normalRelativeAngle(Utils.normalAbsoluteAngle(Math.atan2(ePos.x-w.origin.x, ePos.y - w.origin.y)) - w.absBearing);
				gunWaves.remove(w);
			}
		}
	}
	
	/*
	 * This method lets us project one point from another given a specific angle and distance.
	 */
	public Point2D.Double project(Point2D.Double origin, double dist, double angle){
		return new Point2D.Double(origin.x + dist * Math.sin(angle), origin.y + dist * Math.cos(angle));
	}
	
	/*
	 * This is where we will paint our waves;
	 */
	public void paint(){
		Graphics g=getGraphics();
		double radius;
 
		/*
		 * Paints the waves and the imaginary bullets from the movement.
		 */
		if(PAINT_MOVEMENT){
			for(int i = 0; i < moveWaves.size(); i++){
				MovementWave w = moveWaves.get(i);
				g.setColor(Color.blue);
				radius = (getTime() - w.startTime) * w.speed + w.speed;
				g.drawOval(
						(int)(w.origin.x - radius), 
						(int)(w.origin.y - radius),
						(int)radius * 2,
						(int)radius * 2);
				LinkedList<Point2D.Double> bulletPositions = new LinkedList<Point2D.Double>();
				bulletPositions.add(project(w.origin, radius, w.angle + SHOOTING_ANGLE));
				bulletPositions.add(project(w.origin, radius, w.angle));
				bulletPositions.add(project(w.origin, radius, w.angle - SHOOTING_ANGLE));
				g.setColor(Color.red);
				for (Point2D.Double bulletPosition: bulletPositions)
				{
					g.fillOval((int)bulletPosition.x-3,(int)bulletPosition.y-3,6,6);
				}
			}
		}
		
		/*
		 * Just paints the waves for the targeting.
		 */
		if(PAINT_GUN){
			for(int i = 0; i < gunWaves.size(); i++){
				GunWave w = gunWaves.get(i);
				g.setColor(Color.blue);
				radius = (getTime() - w.startTime) * w.speed;
				g.drawOval((int)(w.origin.x - radius), (int)(w.origin.y - radius), (int)radius * 2, (int)radius * 2);
			}
		} 
	}
	
	/*
	 * Calculate a heuristic to evaluate the certainty that a fired bullet will hit the target.
	 * The heuristic is based on how far the target is away and how fast it is moving.
	 */
	public double aimingCertainty(ScannedRobotEvent e)
	{
		double diagonal = Math.sqrt(Math.pow(getBattleFieldHeight(), 2) + Math.pow(getBattleFieldWidth(), 2));
		double maxDistanceMeasure = e.getDistance() / diagonal;
		double maxSpeedMeasure = Math.abs(e.getVelocity()) / MAX_SPEED;
		
		return ((1 - maxDistanceMeasure) + (1 - maxSpeedMeasure)) / 2f;
	}
	
	/*
	 * This class is the data we will need to use our movement waves.
	 */
	public static class MovementWave{
		Point2D.Double origin;
		double startTime;
		double speed;
		double angle;
		double latVel;
	}
	
	/*
	 * This class is the data we will need to use for our targeting waves.
	 */
	public class GunWave{
		double speed;
		Point2D.Double origin;
		int velSeg;
		double absBearing;
		double startTime;
	}
 
}
